﻿using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Input;

namespace WinHooksNET
{
    public class MouseHook
    {
        #region Members
        public event EventHandler<Events.MouseHookEventArgs> OnMouseAction;

        private List<Events.MouseHookEventArgs> _eventsHistory = new List<Events.MouseHookEventArgs>();

        private Helpers.PInvokeCode.HookProc _mouseHookProcHandle;
        private Structs.MouseCoords _mouseCoords;
        private Structs.MouseCoords _lastMouseCoords;
        private IntPtr _mouseHookHandle;

        private bool _handleMouseButtonUp = true;
        private bool _handleMouseButtonDown = true;
        private bool _handleMouseMove = true;
        private bool _handleMouseWheel = true;

        private Enums.KeyState _leftButtonState = Enums.KeyState.Released;
        private Enums.KeyState _rightButtonState = Enums.KeyState.Released;

        private Enums.FilterMode _filterMode = Enums.FilterMode.BooleanFilters;

        private int _historyLimit = 100;

        private Func<bool> _customFilterFunc;
        
        /// <summary>
        /// Determine whether the library is to intercept mouse button up
        /// </summary>
        public bool HandleMouseButtonUp
        {
            get { return _handleMouseButtonUp; }
            set { _handleMouseButtonUp = value; }
        }

        /// <summary>
        /// Determine whether the library is to intercept mouse button down
        /// </summary>
        public bool HandleMouseButtonDown
        {
            get { return _handleMouseButtonDown; }
            set { _handleMouseButtonDown = value; }
        }

        /// <summary>
        /// Determine whether the library is to intercept mouse button move
        /// </summary>
        public bool HandleMouseMove
        {
            get { return _handleMouseMove; }
            set { _handleMouseMove = value; }
        }

        /// <summary>
        /// Determine whether the library is to intercept mouse wheel
        /// </summary>
        public bool HandleMouseWheel
        {
            get { return _handleMouseWheel; }
            set { _handleMouseWheel = value; }
        }

        /// <summary>
        /// Determine Filter Mode. BooleanFilter for bool variables or CustomFunction for CustomFilterFunc var.
        /// </summary>
        public Enums.FilterMode FilterMode
        {
            get { return _filterMode; }
            set { _filterMode = value; }
        }

        /// <summary>
        /// Custom function used by filter. FilterMode must be set to a CustomFunction
        /// </summary>
        public Func<bool> CustomFilterFunc
        {
            get { return _customFilterFunc; }
            set { _customFilterFunc = value; }
        }

        /// <summary>
        /// List of called events. Limit of items can be set in HistoryLimit var
        /// </summary>
        public List<Events.MouseHookEventArgs> EventsHistory
        {
            get { return _eventsHistory; }
            set { _eventsHistory = value; }
        }

        /// <summary>
        /// Limit of items in called event history. If 0, history can be empty
        /// </summary>
        public int HistoryLimit
        {
            get { return _historyLimit; }
            set
            {
                _historyLimit = value;
                if (EventsHistory.Count >= HistoryLimit)
                {
                    EventsHistory.RemoveRange(HistoryLimit, EventsHistory.Count - HistoryLimit);
                }
            }
        }

        public Structs.MouseCoords MouseCoords
        {
            get { return _mouseCoords; }
        }

        public Enums.KeyState LeftMouseButtonState
        {
            get { return _leftButtonState; }
        }

        public Enums.KeyState RightMouseButtonState
        {
            get { return _rightButtonState; }
        }
        #endregion

        public MouseHook()
        {
            Helpers.PInvokeCode.WinApiPoint cursorPoint = new Helpers.PInvokeCode.WinApiPoint();
            Helpers.PInvokeCode.GetCursorPos(ref cursorPoint);
            _lastMouseCoords = Helpers.Tools.ConvertPointToMouseCoords(cursorPoint);

            _mouseHookProcHandle = new Helpers.PInvokeCode.HookProc(MouseHookProc);

            //Register mouse hook
            _mouseHookHandle = Helpers.PInvokeCode.SetWindowsHookEx(Helpers.PInvokeCode.HookType.WH_MOUSE_LL, 
                _mouseHookProcHandle, Helpers.Tools.GetCurrentModuleHandle(), 0);

            //If returned handle is smaller than 0, throw exception
            if ((int)_mouseHookHandle == 0)
                throw new Exceptions.CreateHookException("Returned mouse hook handle is null");

            _customFilterFunc = new Func<bool>(() => { return true; });
        }

        #region Functions
        private IntPtr MouseHookProc(int code, IntPtr wParam, IntPtr lParam)
        {
            //If code is smaller than 0, this is error :/
            if (code >= 0)
            {
                //Get mouse action from wParam
                Helpers.PInvokeCode.MouseSignal mouseAction = (Helpers.PInvokeCode.MouseSignal)wParam;

                //Get mouse data from lParam and save it to mouseData
                Helpers.PInvokeCode.WinApiMouseDataStruct mouseData = (Helpers.PInvokeCode.WinApiMouseDataStruct)Marshal.PtrToStructure(
                lParam, typeof(Helpers.PInvokeCode.WinApiMouseDataStruct));

                //Get mouse wheel data
                int mouseWheelData = (int)mouseData.mouseData >> 16;

                //Init wheelAction
                Enums.MouseWheelAction wheelAction = Enums.MouseWheelAction.Unknown;

                //If mouse action is WM_MOUSEWHEEL
                if (mouseAction == Helpers.PInvokeCode.MouseSignal.WM_MOUSEWHEEL)
                {
                    //If mouseWheelData is bigger than 0, wheel is go up, if not, wheel is go down
                    if (mouseWheelData > 0) { wheelAction = Enums.MouseWheelAction.WheelUp; }
                    else { wheelAction = Enums.MouseWheelAction.WheelDown; }
                }

                //Get mouse coords from point
                Structs.MouseCoords coords = Helpers.Tools.ConvertPointToMouseCoords(mouseData.pt);
                _mouseCoords = coords;

                //Calculate mouse coords delta
                Structs.MouseCoords delta = new Structs.MouseCoords()
                {
                    X = coords.X - _lastMouseCoords.X,
                    Y = coords.Y - _lastMouseCoords.Y
                };

                //Update last mouse coords
                _lastMouseCoords = coords;

                //Init MouseAction enum
                Enums.MouseAction action = new Enums.MouseAction();

                //Set action from mouseAction (wParam)
                if (mouseAction == Helpers.PInvokeCode.MouseSignal.WM_LBUTTONDOWN)
                { action = Enums.MouseAction.LeftButtonPressed; _leftButtonState = Enums.KeyState.Pressed; }
                    
                else if (mouseAction == Helpers.PInvokeCode.MouseSignal.WM_LBUTTONUP)
                { action = Enums.MouseAction.LeftButtonReleased; _leftButtonState = Enums.KeyState.Released; }
                    
                else if (mouseAction == Helpers.PInvokeCode.MouseSignal.WM_RBUTTONDOWN)
                { action = Enums.MouseAction.RightButtonPressed; _rightButtonState = Enums.KeyState.Pressed; }
                    
                else if (mouseAction == Helpers.PInvokeCode.MouseSignal.WM_RBUTTONUP)
                { action = Enums.MouseAction.RightButtonReleased; _rightButtonState = Enums.KeyState.Released; }
                   
                else if (mouseAction == Helpers.PInvokeCode.MouseSignal.WM_MOUSEMOVE) 
                { action = Enums.MouseAction.MouseMove; }
                    
                else if (mouseAction == Helpers.PInvokeCode.MouseSignal.WM_MOUSEWHEEL) 
                { action = Enums.MouseAction.MouseWheel; }

                //If this action is allowed, go next
                if (IsActionAllowed(mouseAction))
                {
                    Events.MouseHookEventArgs eventArgs = new Events.MouseHookEventArgs()
                    {
                        Action = action,
                        WheelAction = wheelAction,
                        Coords = coords,
                        DeltaCoords = delta,
                        Time = DateTime.Now.Ticks
                    };

                    AddEventToHistory(eventArgs);
                    //Call event with parameters
                    OnMouseAction(this, eventArgs);
                }
            }

            return Helpers.PInvokeCode.CallNextHookEx(_mouseHookHandle, code, wParam, lParam);
        }

        private bool IsActionAllowed(Helpers.PInvokeCode.MouseSignal action)
        {
            if (_filterMode == Enums.FilterMode.BooleanFilters)
            {
                if (((action == Helpers.PInvokeCode.MouseSignal.WM_LBUTTONDOWN || action == Helpers.PInvokeCode.MouseSignal.WM_RBUTTONDOWN) && HandleMouseButtonDown) ||
                    ((action == Helpers.PInvokeCode.MouseSignal.WM_LBUTTONUP || action == Helpers.PInvokeCode.MouseSignal.WM_RBUTTONUP) && HandleMouseButtonUp) ||
                    ((action == Helpers.PInvokeCode.MouseSignal.WM_MOUSEMOVE) && HandleMouseMove) ||
                    ((action == Helpers.PInvokeCode.MouseSignal.WM_MOUSEWHEEL) && HandleMouseWheel))
                {
                    return true;
                }
            }
            else if (_filterMode == Enums.FilterMode.CustomFunction)
            {
                bool result = _customFilterFunc();
                return result;
            }

            return false;
        }

        private void AddEventToHistory(Events.MouseHookEventArgs _eventArgs)
        {
            if (HistoryLimit != 0)
            {
                if (EventsHistory.Count >= HistoryLimit)
                {
                    if (EventsHistory.Count > 0)
                    {
                        EventsHistory.RemoveAt(EventsHistory.Count - 1);
                    }
                }
                EventsHistory.Insert(0, _eventArgs);
            }
        }
        #endregion

        /// <summary>
        /// Dispose and unhook all hooks
        /// </summary>
        public void Dispose()
        {
            //Unhook mouse hook
            Helpers.PInvokeCode.UnhookWindowsHookEx(_mouseHookHandle);
        }
    }
}
